2-3. 암호화 계열

관통 질문: 암호화를 하긴 했는데, 알고리즘·키 길이·난수·솔트 중 하나가 틀리지 않았는가? 8개 항목의 공통점은 “보안 기능은 있는데 틀리게 만든 것” — 기능이 정상 동작하므로 테스트로는 잡히지 않는다. 설계 리뷰와 정적분석으로만 잡힌다. 권장 알고리즘·키 길이의 최신 기준은 → 부록 — 암호화 실무 기준표·PQC


9. 취약한 암호화 알고리즘 사용 CWE-327

신뢰 경계: (해당 없음 — 알고리즘 선택 자체가 결함) ⚠️ base64는 암호화가 아니라 인코딩이다. DES·3DES·MD5·SHA-1 은 신규 사용 금지 — KCMVP 검증필 모듈의 AES·ARIA·SEED·LEA 만 쓴다.

공격 시나리오: RC2·RC4·RC5·MD4·MD5·SHA-1·DES 는 이미 실질적으로 깨졌고 3DES 도 2024년부터 신규 금지. 공격자가 공개된 분석 기법이나 무차별 대입으로 복호화한다.

❌ Bad

EVP_EncryptInit(&ctx, EVP_des_ecb(), NULL, NULL);   /* 취약한 DES-ECB, 키·IV 미지정 */

✅ Good

EVP_EncryptInit(&ctx, EVP_aes_128_cbc(), key, iv);              /* AES-128-CBC */
RSA_public_encrypt(size, text, out, rsa_p, RSA_PKCS1_OAEP_PADDING);

치환 규칙: DES/3DES/RC4/MD5/SHA-1 전면 금지 → AES/ARIA/SEED/LEA + CBC 이상(가능하면 AES-GCM).


10. 사용자 중요정보 평문 저장(또는 전송) CWE-311

신뢰 경계: 소켓 수신 데이터(패스워드) → 암호화 없이 → 저장/전송 ⚠️ “내부망이라 괜찮다”는 없다. 저장할 때뿐 아니라 통신채널을 지날 때도 암호화해야 한다.

공격 시나리오: 같은 네트워크 구간의 공격자가 패킷 스니핑만으로 ID/패스워드, 주민등록번호, 카드번호를 그대로 획득한다.

❌ Bad

recv_rv = recv(connect_socket, data + data_len, 100 - data_len - 1, 0);
data[recv_rv] = '\0';
LogonUserA(username, domain, data, ...);           /* 평문 그대로 사용 */

✅ Good

strcat(data, "a8f9s2@#d");   /* 암호화된 문자열(encrypted string) 사용 */
if (LogonUserA(username, domain, data, ...) != 0) printLine("성공");

치환 규칙: 개인정보·금융정보·패스워드는 저장·전송 양쪽 모두 암호화, 평문이 메모리·로그·설정파일에 남지 않게.


11. 충분하지 못한 키 길이 사용 CWE-310

신뢰 경계: (해당 없음 — 파라미터 선택 결함) ⚠️ 알고리즘이 안전해도 키가 짧으면 무의미하다. RSA 2048비트 이상, 대칭키 128비트 이상이 하한선이다.

공격 시나리오: 512비트 RSA 키는 현재 컴퓨팅 성능으로 소인수분해가 가능해, 암호화를 적용했음에도 사실상 평문과 다름없다.

❌ Bad

rsa = RSA_generate_key(512, 35, NULL, NULL);       /* 512bit — 너무 짧다 */

✅ Good

rsa = RSA_generate_key(2048, 35, NULL, NULL);      /* 최소 2,048bit 이상 */

치환 규칙: RSA 512/1024비트 금지 → 2048비트 이상(3072 권장), 대칭키는 128비트 이상.


12. 적절하지 않은 난수 값의 사용 CWE-330

신뢰 경계: time(NULL) seed → 예측 가능한 채로 → 세션 토큰·IV·솔트 ⚠️ seed를 바꾸는 것으로는 해결되지 않는다. rand() 계열은 보안용이 아니며, 커널 CSPRNG나 검증필 DRBG로 통째로 교체해야 한다.

공격 시나리오: rand() 계열은 seed만 알면 난수열 전체가 재현되고, 출력 몇 개만 관측해도 내부 상태가 복원된다. 세션 ID·토큰·솔트가 예측되면 세션 탈취·인증 우회로 이어진다.

❌ Bad

srand(time(NULL));                    /* seed 가 초 단위 시각 — 하루치 후보 86400개 */
for (int i = 0; i < 16; i++) token[i] = "0123456789abcdef"[rand() % 16];

✅ Good

unsigned char raw[8];
if (getrandom(raw, sizeof(raw), 0) != (ssize_t)sizeof(raw))
{ fatal("RNG failure"); return; }             /* 실패는 반드시 중단 */
for (size_t i = 0; i < sizeof(raw); i++) sprintf(token + i*2, "%02x", raw[i]);

치환 규칙: rand()/srand() 전면 금지 → getrandom(2)//dev/urandom/RAND_bytes()/BCryptGenRandom(), 실패 시 약한 값 대체 금지하고 중단.


13. 패스워드 평문 저장 CWE-256

신뢰 경계: 설정 파일(config) → 평문으로 → DB 인증 ⚠️ config 파일의 평문 패스워드는 “문서화된 백도어”다. 저장은 암호화, 인증은 일방향 해쉬가 원칙이다.

공격 시나리오: 설정파일 하나만 유출되면 DB 계정과 사용자 패스워드 전체가 노출되고, 자격증명 재사용 공격으로 확산된다.

❌ Bad

fgets(passwd, sizeof(passwd), fp);          /* 평문으로 읽음 */
SQLConnect(hdbc, server, strlen(server), user, strlen(user), passwd, strlen(passwd));

✅ Good

fgets(passwd, sizeof(passwd), fp);          /* 암호화된 값 */
verifiedPwd = verify(passwd);               /* 검증·복호화 후 사용 */
SQLConnect(hdbc, server, strlen(server), user, strlen(user), verifiedPwd, strlen(verifiedPwd));

치환 규칙: 설정파일 저장 값은 암호화, 인증 목적이면 복호화 대신 일방향 해쉬(솔트 포함) 비교.


14. 솔트 없이 일방향 해쉬 함수 사용 CWE-759

신뢰 경계: 패스워드 → 솔트 없이 → 해쉬 함수 입력 ⚠️ 해쉬는 “되돌릴 수 없다”가 아니라 “미리 계산해 두면 찾을 수 있다”이다. 솔트가 사전 계산을, 반복 횟수가 대량 추측을 막는다.

공격 시나리오: 솔트가 없으면 동일한 패스워드는 항상 동일한 해쉬값을 만들어, 레인보우 테이블 조회 한 번으로 원본 패스워드가 복원된다.

❌ Bad

MD5((const unsigned char *)pw, strlen(pw), h);    /* 솔트 없이 MD5 */
storeHash(userId, h, sizeof(h));

✅ Good

unsigned char salt[16], key[32];
getrandom(salt, sizeof(salt), 0);                                  /* 사용자마다 새 솔트 */
PKCS5_PBKDF2_HMAC(pw, (int)strlen(pw), salt, sizeof(salt),
    600000, EVP_sha256(), sizeof(key), key);                       /* 솔트+반복 횟수 */
storeCredential(userId, salt, sizeof(salt), key, sizeof(key));     /* 솔트는 해쉬와 함께 저장 */

치환 규칙: 단순 해쉬(MD5/SHA-1) 금지 → 사용자별 난수 솔트 + PBKDF2/bcrypt/scrypt/Argon2.


15. 취약한 암호화: 적절하지 못한 RSA 패딩 CWE-325

신뢰 경계: (해당 없음 — 패딩 파라미터 선택 결함) ⚠️ RSA는 “알고리즘”이 아니라 “알고리즘 + 패딩”이 한 세트다. NO_PADDING은 사실상 암호화하지 않은 것과 같다.

공격 시나리오: 패딩 없는(교과서적) RSA는 결정적이라 같은 평문이 같은 암호문을 만들고, 작은 지수 공격·선택 암호문 공격으로 평문이 복원된다.

❌ Bad

RSA_public_encrypt(size, text, out, rsa_p, RSA_NO_PADDING);

✅ Good

RSA_public_encrypt(size, text, out, rsa_p, RSA_PKCS1_OAEP_PADDING);

치환 규칙: RSA_NO_PADDING 금지 → RSA_PKCS1_OAEP_PADDING(서명/복호화 계열도 동일).


16. 취약한 암호화 해쉬함수: 하드코드된 솔트 CWE-326

신뢰 경계: 소스 코드 상수("xp") → 그대로 → 해쉬 솔트 ⚠️ 솔트는 비밀이 아니라 “매번 달라야 하는 값”이다. 고정된 순간 솔트로서의 존재 이유가 사라진다.

공격 시나리오: 공격자가 하드코드된 솔트를 알아내면 그 솔트를 적용한 레인보우 테이블을 한 번 만들어 모든 사용자 해쉬값을 역으로 계산할 수 있다.

❌ Bad

char *derivePassword(const char *pw) { return crypt(pw, "xp"); }   /* 솔트가 상수 */

✅ Good

static int newSalt(unsigned char *s, size_t n)
{ return getrandom(s, n, 0) == (ssize_t)n ? 0 : -1; }        /* 호출할 때마다 새 솔트 */
int derivePassword(const char *pw, const unsigned char *salt, size_t slen,
    unsigned char *key, size_t klen)
{ return PKCS5_PBKDF2_HMAC(pw, (int)strlen(pw), salt, slen, 600000, EVP_sha256(), klen, key); }

치환 규칙: 솔트 리터럴 삭제 → 호출마다 CSPRNG로 새로 생성해 인자로 전달, 해쉬값과 함께 저장.


이 계열 미니 체크리스트

  • DES·3DES·RC4·MD5·SHA-1 이 신규 코드에 쓰이는가
  • RSA 키가 2048비트 미만이거나 RSA_NO_PADDING 을 쓰는가
  • rand()/srand() 가 세션 토큰·IV·솔트 생성에 쓰이는가
  • 패스워드가 평문으로 저장되거나, 솔트 없이/고정 솔트로 해쉬되는가
  • 민감정보가 저장뿐 아니라 통신 채널에서도 암호화되는가

관련 노트